﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using ImageProcessing.Detectors;
using ImageProcessing.Common;
using ImageProcessing.Descriptors;
using ImageProcessing.Matchers;
using ImageProcessing.PointsSampler;
using ImageProcessing.Storage;
using Common;

namespace ImageProcessing.Engine
{
    public class ImageProcessingEngine : BaseImageProcessingEngine
    {
        private bool _camIntialized;
        private bool _disposed;

        // options
        private readonly CameraMode _camMode = CameraMode.argb;

        public DetectorType DetectorType { get { return _detectType; } }

        public const string Scanning = "Scanning.........";

        private bool _learnInProgress;
        private readonly List<InterestPoint> _learntInterestPoints = new List<InterestPoint>();
        private readonly IMatcher _matcher = new HandlerNearestNeighbour();
        private readonly IMatcher _regionMatcher = new HandlerNearestNeighbour();

        private Thread _argbFramesThread;

        private readonly Stopwatch _captureSw = new Stopwatch();
        private readonly Stopwatch _selectorSw = new Stopwatch();
        private readonly Stopwatch _descriptorSw = new Stopwatch();
        private readonly Stopwatch _matcherSw = new Stopwatch();

        private readonly int _width;
        private readonly int _height;

        private int[] _learntArgbFrame;
        private byte[] _learntYFrame;

        private int[] _argbFrame;
        private byte[] _yFrame;

        public IImageArchiver ImageArchiver { get; set; }
        public bool SaveAddedImagesLocaly = false;

        private readonly IInterestPointDetector _ipd;

        private List<InterestPoint> _ipts = new List<InterestPoint>();
        private readonly List<InterestPoint> _processedIpts = new List<InterestPoint>();
        private readonly List<InterestPoint> _lastMatchedIpts = new List<InterestPoint>();

        private readonly ManualResetEvent _uiSyncEvent;

        public event Action<ImageScanResultsDTO> DescriptorsUpdate;
        public event Action<string> StatusUpdater;
        public event Action LearnComplete;

        public event Action<int[]> SnapShotCapturedARGB;
        public event Action<byte[]> SnapShotCapturedY;

        private readonly Action<int[]> _getSnapShotArbg;
        private readonly Action<byte[]> _snapShotGreyScale;

        public InterestPointsSamplerFactory.SampleType SampleType { get; set; }
        public int MinSamplePoints { get; set; }
        
        private IInterestPointSampler _iips;

        private IInterestPointSampler _learningPointsSampler = InterestPointsSamplerFactory.GetInterestPointSampler(InterestPointsSamplerFactory.SampleType.Stepped);
        private readonly ManualResetEvent _learningBlockingEvent = new ManualResetEvent(false);

        private PointsResultProcessor _prp;

        public IRemoteLookupService Wp7OrtProxy { get; set; }

        public ImageProcessingEngine(int width, int height, Action<int[]> snapShotARGB, Action<byte[]> snapShotGreyScale, ManualResetEvent uiSyncEvent,
            CameraMode camMode = CameraMode.y, DetectorType detectorType = DetectorType.FastCorner)
            : base(detectorType)
        {
            _camMode = camMode;
            _getSnapShotArbg = snapShotARGB;
            _snapShotGreyScale = snapShotGreyScale;
            _width = width;
            _height = height;
            _argbFrame = new int[_width * _height];
            _yFrame = new byte[_width * _height];
            _learntArgbFrame = new int[_width * _height];
            _learntYFrame = new byte[_width * _height];
            _ipd = BuildDetector(_detectType, _width, _height);
            _uiSyncEvent = uiSyncEvent;
            SampleType = InterestPointsSamplerFactory.SampleType.LastMatched;
            MinSamplePoints = 50;
            _learningPointsSampler.MaxPointsDescriptor = 150;
        }

        public void Start()
        {
            if (UseRemoteServices)
            {
                Wp7OrtProxy.PointsFound += RemoteServicePointsFound;
                Wp7OrtProxy.ImageAdded += ImageAdded;
            }

            // start processing frame processing pump
            _argbFramesThread = new Thread(PumpARGBFrames) { IsBackground = true };
            _argbFramesThread.Start();
        }

        private void ImageAdded(TagInfo tagInfo)
        {
            RaiseStatusUpdated("Image Added : " + tagInfo.TagName);
        }

        private void RemoteServicePointsFound(Dictionary<TagInfo, List<PointInfo>> pointsFound)
        {
            foreach (var kvp in pointsFound)
            {
                TrainAndSave(PersistanceHelpers.BuildInterestPoints(kvp.Value));
            }
        }

        // ARGB frame pump
        private void PumpARGBFrames()
        {
            _iips = InterestPointsSamplerFactory.GetInterestPointSampler(SampleType);

            // Create capture buffer.
            _camIntialized = true;
            _uiSyncEvent.Set();

            RaiseStatusUpdated(Scanning);

            try
            {
                while (!_disposed)
                {
                    _uiSyncEvent.WaitOne();

                    var doLearning = _learnInProgress;

                    if (!_camIntialized)
                        return;

                    _uiSyncEvent.Reset();

                    _ipts.Clear();
                    _processedIpts.Clear();

                    _prp = null;

                    _captureSw.Start();

                    // note : camera call typically takes about  argb = 190 ms amd lumanice(y) = 4ms
                    switch (_camMode)
                    {
                        case CameraMode.argb:
                            _getSnapShotArbg(_argbFrame);
                            _ipts = _ipd.GetInterestPoints(ref _argbFrame);
                            break;

                        case CameraMode.y:
                            _snapShotGreyScale(_yFrame);

                            if (UseRemoteLearning)
                            {
                                _getSnapShotArbg(_learntArgbFrame);
                                _yFrame.CopyTo(_learntYFrame, 0);
                            }

                            RaiseSnapshotY(ref _yFrame);
                            _ipts = _ipd.GetInterestPoints(ref _yFrame);
                            break;

                        default:
                            throw new NotImplementedException();
                    }
                    _captureSw.Stop();

                    if (_ipts.Count > MinSamplePoints)
                    {
                        SelectPoints(doLearning);

                        _lastMatchedIpts.Clear();
                        AddDescriptors(_processedIpts, null);
                        MatchOrLearnPoints(doLearning);
                    }
                    else
                    {
                        _descriptorSw.Reset();
                    }

                    if (!doLearning)
                        RaiseDescriptorUpdate();

                    ResetTimers();
                }

            }
            catch (Exception e)
            {
                RaiseStatusUpdated(e.Message + Environment.NewLine + e.StackTrace);
            }
        }

        private bool UseRemoteLearning
        {
            get { return _learnInProgress && Wp7OrtProxy != null; }
        }

        private bool UseRemoteServices
        {
            get { return Wp7OrtProxy != null; }
        }

        private void ResetTimers()
        {
            _captureSw.Reset();
            _selectorSw.Reset();
            _matcherSw.Reset();
        }

        private void AddDescriptors(List<InterestPoint> points, TagInfo tagInfo)
        {
            _descriptorSw.Reset();
            _descriptorSw.Start();
            SurfDescriptor.DecribeInterestPoints(points, false, false, _ipd.IntegralImage, tagInfo);
            _descriptorSw.Stop();
        }

        private void SelectPoints(bool doLearning)
        {
            _selectorSw.Start();

            if (!doLearning)
                _processedIpts.AddRange(_iips.GetSamplePoints(_descriptorSw.ElapsedMilliseconds, _ipts, _lastMatchedIpts));
            else
            {
                IEnumerable<InterestPoint> selectedPoints = _learningPointsSampler.GetSamplePoints(0, _ipts, null);
                _learntInterestPoints.AddRange(selectedPoints);
                _processedIpts.AddRange(selectedPoints);
                RaiseStatusUpdated(String.Format("Learning {0} points", _learntInterestPoints.Count));
            }

            _selectorSw.Stop();
        }

        private void MatchOrLearnPoints(bool doLearning)
        {
            _matcherSw.Start();

            if (doLearning)
            {
                RaiseStatusUpdated(String.Format("Learning {0} points", _learntInterestPoints.Count));
                _learnInProgress = false;
                RaiseLearnComplete();
                RaiseDescriptorUpdate();
                _learningBlockingEvent.WaitOne();
            }
            else
            {
                _prp = new PointsResultProcessor(MinFoundPoints, _processedIpts, _matcher, _regionMatcher);
                _prp.ProcessPoints();

                if (_prp.HasPrimaryMatcherThresholdReached)
                    UpdateRegionHnn(_prp.BestFoundObject);

                if (ShouldWeUseRemoteLookup())
                    LookupWithRemoteService();
            }

            _matcherSw.Stop();
        }

        private bool ShouldWeUseRemoteLookup()
        {
            return UseRemoteServices && (_prp.BestFoundObject == null ||
                                         (_prp.BestFoundObject != null && _prp.BestFoundObject.FoundPointsCount < MinFoundPoints));
        }

        private void LookupWithRemoteService()
        {
            var pointsList = _processedIpts.ToList();
            List<PointInfo> pi = new List<PointInfo>();

            foreach (var point in pointsList)
            {
                pi.Add(point.PointInfo);
            }

            Wp7OrtProxy.LookupPoints(pi);

        }

        private void UpdateRegionHnn(FoundObject foundObject)
        {
            var rs = new RegionSelect(foundObject.FoundPoints);
            var regionPoints = rs.GetPointsWithinRegionOfImage(_ipts);

            var regionPointsToTrain = AddRegionPoints(foundObject.TagInfo, regionPoints, true);

            _regionMatcher.Clear();
            _regionMatcher.Train(regionPointsToTrain);
            _regionMatcher.Train(_lastMatchedIpts);
        }

        private List<InterestPoint> AddRegionPoints(TagInfo tagInfo, List<InterestPoint> regionPoints, bool onlyPointsWithExistingDescptors)
        {
            var descriptorList = new List<InterestPoint>();
            var results = new List<InterestPoint>();
            var pointsCount = regionPoints.Count;

            for (int index = 0; index < pointsCount; index++)
            {
                var interestPoint = regionPoints[index];

                if (onlyPointsWithExistingDescptors)
                {
                    if (!interestPoint.IsValidDescriptor)
                        continue;

                    interestPoint.SetTagInfo(tagInfo);
                }
                else
                    descriptorList.Add(interestPoint);

                results.Add(interestPoint);
            }

            if (!onlyPointsWithExistingDescptors && descriptorList.FirstOrDefault() != null)
                AddDescriptors(descriptorList, tagInfo);

            return results;
        }

        private void RaiseDescriptorUpdate()
        {
            var idr = new ImageScanResultsDTO(_captureSw.ElapsedMilliseconds, _selectorSw.ElapsedMilliseconds, _descriptorSw.ElapsedMilliseconds, _matcherSw.ElapsedMilliseconds, _ipts.Count, _processedIpts.Count, _ipts.ToList(), _prp);
            var evtDescriptorUpdate = DescriptorsUpdate;
            if (evtDescriptorUpdate != null)
                evtDescriptorUpdate(idr);
        }

        private void RaiseStatusUpdated(string status)
        {
            var evtStatusUpdater = StatusUpdater;
            if (evtStatusUpdater != null)
                evtStatusUpdater(status);
        }

        private void RaiseLearnComplete()
        {
            var evtLearnComplete = LearnComplete;
            if (evtLearnComplete != null)
                evtLearnComplete();
        }

        private void RaiseSnapshotY(ref byte[] ySnapshot)
        {
            var evtSnapShot = SnapShotCapturedY;
            if (evtSnapShot != null)
                evtSnapShot(ySnapshot);
        }

        public override void Dispose()
        {
            _camIntialized = false;
            _disposed = true;
        }

        #region Learning

        public void Learn()
        {
            _learnInProgress = true;
        }

        public void SaveLearnt(TagInfo tagInfo)
        {
            List<InterestPoint> savedPoints = _learntInterestPoints.ToList();
            _learntInterestPoints.Clear();

            var savedArgb = _learntArgbFrame;
            var savedY = _learntYFrame;

            AddNameToPoints(tagInfo, savedPoints);

            if (UseRemoteServices)
            {
                RaiseStatusUpdated("Saving image : " + tagInfo.TagName);
                Wp7OrtProxy.AddImage(_width, _height, tagInfo, ref savedArgb, ref savedY);
            }

            if (SaveAddedImagesLocaly && ImageArchiver != null)
                ImageArchiver.SaveRawImage(tagInfo, _width, _height, ref savedY);

            _learningBlockingEvent.Set();

            TrainAndSave(savedPoints);
        }

        public void ManualLearnPoints(IEnumerable<InterestPoint> interestPoints)
        {
            _matcher.Train(interestPoints);
        }

        public void ManualLearnImage(byte[] image, TagInfo tagInfo)
        {
            var points = _ipd.GetInterestPoints(ref image);
            SurfDescriptor.DecribeInterestPoints(points, false, false, _ipd.IntegralImage, tagInfo);
            TrainAndSave(points);
        }

        private void TrainAndSave(List<InterestPoint> interestPoints)
        {
            var tagInfo = interestPoints[0].TagInfo;
            var pointsAdded = _matcher.Train(interestPoints);

            if (DBConnection == null)
                throw new Exception("DB connection not set");

            DBConnection.SavePoints(tagInfo, pointsAdded);
        }

        #endregion

        public int LoadAllPointsFromDB()
        {
            if (DBConnection == null)
                throw new Exception("DB connection not set");

            List<InterestPoint> points = DBConnection.LoadAllPoints();
            _matcher.ImportPoints(points);

            return points.Count();
        }

        public void PurgeAllData()
        {
            if (DBConnection == null)
                throw new Exception("DB connection not set");

            DBConnection.Clear();
            _matcher.Clear();
            _regionMatcher.Clear();
        }
    }

}
